#============ TRYNE_DB - core data logic
import random
from google.appengine.ext import db
from google.appengine.ext.db import polymodel
from google.appengine.ext.db import Expando
from google.appengine.api import users
#import datetime

#============ Operational parameters - set now for 3 users :-)
#cCountersPeriod     = 60*60     # counter update period in seconds, 1 hour [ not less then 24h ]
#cLinkVisibleImps    = 20        # after this number of imps link becomes public (visible to people)
cInitViewsToday     = 2          # Fake "views per day" for new question - to make it initially visible
cFinalStage         = 777        # demi-value to display that question survey is passed
rndMax              = 100*10000  # Max value to represent random field
cImportantFact      = 40         # likes percentage for fact to be considered important
cMaxAnswers         = 1000       # Max number of concurrent active answers to one question
cDeleteBadEntity    = 40         # Bad percentage needed to delete Entity
cDeleteBadEntityN   = 2          # Number of imps before Entity may be deleted
cPublicImps         = 3          # Minimum impressions number for entity to be public


def g(id,**kwargds):
    try: entity = db.get(id,**kwargds)
    except: entity=False
    return entity

#============ Entity dataclass - base database token, parent of Question|Answer|Fact|Tag

class Entity(polymodel.PolyModel):
    # Static data, that is filled at entity creation and doesn't change anymore
    text        = db.StringProperty(multiline=True) # entity name|contents
    date        = db.DateTimeProperty(auto_now_add=True) # creation date
    author      = db.UserProperty() # entity author
    rnd         = db.IntegerProperty(default=rndMax) # random number for selections
    # Dynamic data
    imps        = db.IntegerProperty(default=0) # judging impressions, i.e. how many times ppl seen this entity and judged it
    likes       = db.IntegerProperty(default=0) # likes, for different entities have different exact meaning
    bads        = db.IntegerProperty(default=0) # spam, meaningless or any other way totally inapropriate
    # Calculated data
    like        = db.IntegerProperty(default=0) # likes in imps percentage
    bad         = db.IntegerProperty(default=0) # bad in imps percentage
    public      = db.BooleanProperty(default=False) # Public flag, triggers, when imps > cPublicImps
    def init(self,EntityText):
        log('Entity.init','text = '+EntityText)
        self.text=EntityText
        self.author = users.get_current_user()
        self.rnd = random.randint(0,rndMax)
        return self
    def update(self): # update calculated data and take actions
        # WARNING: Entity may be unaccesible after update()
        if self.imps:
            self.like = self.likes * 100 // self.imps
            self.bad = self.bads * 100 // self.imps
            self.public = self.imps >= cPublicImps
        log('Entity.update',self.text)
        log('imps:'+str(self.imps)+' like:'+str(self.like)+'% bad:'+str(self.bad)+'%')
        if (self.bad>cDeleteBadEntity) and (self.imps>=cDeleteBadEntityN):
            log('DELETED: Entity is decided to be bad')
            self.delete()
        else: self.put()
    def delete(self, **kwargs):
        db.delete(Double.all().filter('entity1',self).fetch(cMaxAnswers))
        db.delete(Double.all().filter('entity2',self).fetch(cMaxAnswers))
        polymodel.PolyModel.delete(self, **kwargs)
    

#============ Question dataclass

class Question(Entity):
    tags        = db.StringListProperty(default=[]) # text tag list
    views       = db.IntegerProperty(default=0) # some statistics
    answers     = db.IntegerProperty(default=0)
    facts     = db.IntegerProperty(default=0)
    nodes     = db.IntegerProperty(default=0) # solution tree nodes
    childs = db.IntegerProperty(default=0) # Number of direct child nodes for this one
    viewsToday  = db.IntegerProperty(default=cInitViewsToday) # Updated once a day
    solved      = db.BooleanProperty(default=False) # Updated once a day

    def delete(self, **kwargs):
        db.delete(Entity.all().filter('owner',self).fetch(cMaxAnswers))
        Entity.delete(self, **kwargs)

#============ Answer dataclass

class Answer(Entity):
    owner       = db.ReferenceProperty(Question) # Question that is an owner of corresponding Answer
    timps       = db.IntegerProperty(default=0)  # number of impressions like one of top-answer
    tlikes      = db.IntegerProperty(default=0)  # number of top selections
    # Calculated data
    tlike       = db.IntegerProperty(default=0)  # tlikes in timps percentage
    
    def init(self,EntityText, EntityOwner):
     Entity.init(self,EntityText)
     log('Answer.init','text = '+EntityText + '  owner = '+str(EntityOwner))
     self.owner = EntityOwner
     return self
    def update(self): # update calculated data and take actions
        # WARNING: Entity may be unaccesible after update()
        if self.timps:
            self.tlike = self.tlikes * 100 // self.timps
        Entity.update(self)

#============ Fact dataclass

class Fact(Entity):
    owner       = db.ReferenceProperty(Question) # Question that is an owner of corresponding Fact

    def init(self,EntityText, EntityOwner):
     Entity.init(self,EntityText)
     log('Fact.init','text = '+EntityText + '  owner = '+str(EntityOwner))
     self.owner = EntityOwner
     return self

#============ Fact dataclass

class Tag(Entity):
    links       = db.IntegerProperty(default=0) # Number of links to this tag

    def link(self,question): # link tag to question
     if question.tags.count(self.text): return
     question.tags.append(self.text)
     self.links+=1
    def unlink(self,question): # link tag to question
     question.tags.remove(self.text)
     self.links-=1
    def delete(self, **kwargs):
        quests=Question.all().filter('tags',self.text).fetch(cMaxAnswers)
        for quest in quests:
            quest.tags.remove(self.text)
        db.put(quests)
        Entity.delete(self, **kwargs)

# ============================ SOLUTION TREE codes ===============================================

#============ Node dataclass
# Solution tree node, that holds a solution step

class Node(Entity):
    owner    = db.ReferenceProperty(Question) # Question that is an owner of corresponding node
    previous = db.ReferenceProperty(Entity) # Parent Node of this node. Also previous solution step.
    childs = db.IntegerProperty(default=0) # Number of direct child nodes for this one
    # Parent is Question for first steps.
    def init(self,EntityText, EntityOwner, PreviousNode):
        Entity.init(self,EntityText)
        log('Node.init','text = '+EntityText + '  owner = '+str(PreviousNode))
        self.owner = EntityOwner
        self.previous = PreviousNode
        PreviousNode.childs+=1
        PreviousNode.put()
        return self
    def delete(self, **kwargs):
        nodes=Nodes.all().filter('previous',self).fetch(cMaxAnswers)
        for node in nodes:
            node.delete()
        previous.get()
        PreviousNode.childs-=1
        PreviousNode.put()
        Entity.delete(self, **kwargs)

# ============================ END SOLUTION TREE code ===============================================

#============ Link base dataclass - Links between two entities, that is a subject for judgement. Now is used as base for tags and doubles

class Link(polymodel.PolyModel):
    entity1     = db.ReferenceProperty(Entity,collection_name='link_entities1')  # Links to Entities, Entity1<Entity2, or Entity1 is parent of Entity2 always
    entity2     = db.ReferenceProperty(Entity,collection_name='link_entities2')
    imps        = db.IntegerProperty(default=0) # judging impressions

class Double(Link):
    likes1      = db.IntegerProperty(default=0) # entity1 was judged better
    likes2      = db.IntegerProperty(default=0) # entity2 was judged better

class Tagger(Link): # entity1=Tag entity2=Question
    bads      = db.IntegerProperty(default=0) # tag was judged to be bad

#============ Impression dataclass - Questions, that was shown to users

class Impression(db.Model):
    question    = db.ReferenceProperty(Entity)              # Link to question
    date        = db.DateTimeProperty(auto_now_add=True)    # show date
    user        = db.UserProperty()                         # user (spectator-judge)
    stage       = db.IntegerProperty(default=0)             # entity show stage
    anoid       = db.IntegerProperty(default=0)             # Anonymous user ID

#============ TUser dataclass (Tryne user) - user specific data

class TUser(db.Model):
    user        = db.UserProperty()                         # user (spectator-judge)
    rnd         = db.IntegerProperty(default=0)             # save the randomizer state for every user to avoid tricky updates


#============= Entities comparizon code

def TagVote(tag,quest,good=False):
    link = Tagger.all().filter('entity1',tag).filter('entity2',quest).get()
    if not link:
        link=Tagger()
        link.entity1 = tag
        link.entity2 = quest
    link.imps+=1
    if not good: link.bads+=1
    if link.imps>=cDeleteBadEntityN:
        if link.bads*100//link.imps>cDeleteBadEntity:
            link.delete()
            tag.unlink(quest)
            quest.put()
            return
    if link.bads: link.put()

def DuplicateVote(win,loose,draw=False):
    versa = False
    if win.key>loose.key:
        win,loose = loose,win
        versa = True
    link = Double.all().filter('entity1',win).filter('entity2',loose).get()
    if not link:
        link=Double()
        link.entity1 = win
        link.entity2 = loose
    link.imps+=1
    if not draw:
        if versa: link.likes2+=1
        else: link.likes2+=1
    if link.imps>=cDeleteBadEntityN:
        if abs(link.likes1-link.likes2)*100//link.imps>cDeleteBadEntity:
            link.delete()
            if versa: DeleteDuplicate(loose,win)
            else: DeleteDuplicate(win,loose)
            return
    if link.imps>1 and not draw: link.put()

def DeleteDuplicate(win,loose):
    log('DeleteDuplicate',str(loose))
    if win.class_name()=='Question':
        # merge tags
        for tag in loose.tags:
            if not win.tags.count(tag):
                win.tags.append(tag)
        win.put()
        ents = Entity.all().filter('owner',loose).fetch(cMaxAnswers)
        for ent in ents:
            ent.owner = win
        db.put(ents)
    elif win.class_name()=='Tag': # we will override regular Tag delete
        quests=Question.all().filter('tags',loose.text).fetch(cMaxAnswers)
        for quest in quests:
            quest.tags.remove(loose.text)
            quest.tags.append(win.text)
        db.put(quests)
        win.links+=loose.links
        win.put()
        Entity.delete(loose)
        return
    loose.delete()






compare1=[] # Comparizon variables
compare2=[]

def Compare1():
    global compare1
    return compare1

def Compare2():
    global compare2
    return compare2

def DecideCompare(owner): # Decide 2 entities to compare
    global compare1
    global compare2
    compare1=[]
    compare2=[]
    if owner:
        compare1=Entity.all().filter('owner',owner).filter('rnd >',random.randint(0,rndMax)).order('rnd').get()
        if compare1: compare2=Entity.all().filter('class',compare1.class_name()).filter('owner',owner).filter('rnd >',random.randint(0,rndMax)).order('rnd').get()
    c=0
    while not (compare1 and compare2 and (compare1.text!=compare2.text)): # if we failed to find pair corresponded to owner, lets try to find totally random one
        c+=1
        if c>4: return False
        compare1=Entity.all().filter('rnd >',random.randint(0,rndMax)).order('rnd').get()
        if compare1:
            if (compare1.class_name()=='Answer') or (compare1.class_name()=='Fact'):
                compare2=Entity.all().filter('class',compare1.class_name()).filter('owner',compare1.owner).filter('rnd >',random.randint(0,rndMax)).order('rnd').get()
            else: compare2=Entity.all().filter('class',compare1.class_name()).filter('rnd >',random.randint(0,rndMax)).order('rnd').get()
    return True

def BigCase(st): # Make cases to be first big letter and other small
 s=st[0].upper()+st[1:len(st)].lower()
 return s

def RandomOrder(rlist):
    res = []
    while rlist:
     r = random.randint(0,len(rlist)-1)
     res.append(rlist[r])
     del rlist[r]
    return res


def GetRandomEntities(type,owner,number): # Return <number> random Entities
 log('GetRandomEntities',str(number)+' '+type)
 ents = Entity.all().filter('class',type).filter('owner',owner).fetch(number)
 return RandomOrder(ents)
 #if len(ents)<=number: return ents
 #res = []
 #for i in range(number):
 #   if ents:
 #    r = random.randint(0,len(ents)-1)
 #    res.append(ents[r])
 #    del ents[r]
 #   else: break
 #return res

def PrepareTagName(tag): # strips leading/trailing symbols, converts to lowercase, add '*' if bad
    tag = tag.lower()
    # Strip trailing
    while tag and not (tag[len(tag)-1].isalpha()):
        tag = tag[0:len(tag)-1]
    # Strip leading
    while tag and not tag[0].isalpha():
        tag = tag [1:len(tag)]
    for i in range(len(tag)):
        if not tag[i].isalpha():
            return '*'+tag
    return tag

def FindEntity(type,text): # Find 1 entity with arbitrary text
    log('FindEntity',lc(locals()))
    return Entity.all().filter('class',BigCase(type)).filter('text',text).get()

#============ LOG facility
logs = ''

def log_title(title):
    global logs
    logs+='\n'+title+'\n'
def log_text(text):
    global logs
    logs+='   '+text+'\n'
def log(title,text=''):
    global logs
    if text:
        log_title(title)
        log_text(text)
    else:
        log_text(title)
def get_logs():
    global logs
    return logs
def clear_logs():
    global logs
    logs=''
def lc(locals):
    if 'self' in locals:
        del locals['self']
    return str(locals)

# Let it be here for a while    
def pretty_date(time=False):
    """
    Get a datetime object or a int() Epoch timestamp and return a
    pretty string like 'an hour ago', 'Yesterday', '3 months ago',
    'just now', etc
    """
    from datetime import datetime
    now = datetime.now()
    if type(time) is int:
        diff = now - datetime.fromtimestamp(time)
    elif isinstance(time,datetime):
        diff = now - time 
    elif not time:
        diff = now - now
    second_diff = diff.seconds
    day_diff = diff.days

    if day_diff < 0:
        return ''

    if day_diff == 0:
        if second_diff < 10:
            return "just now"
        if second_diff < 60:
            return str(second_diff) + " seconds ago"
        if second_diff < 120:
            return  "a minute ago"
        if second_diff < 3600:
            return str( second_diff / 60 ) + " minutes ago"
        if second_diff < 7200:
            return "an hour ago"
        if second_diff < 86400:
            return str( second_diff / 3600 ) + " hours ago"
    if day_diff == 1:
        return "Yesterday"
    if day_diff < 7:
        return str(day_diff) + " days ago"
    if day_diff < 31:
        return str(day_diff/7) + " weeks ago"
    if day_diff < 365:
        return str(day_diff/30) + " months ago"
    return str(day_diff/365) + " years ago"

#============ Counters dataclass - hourly system counters storage

#class Counters(db.Model):
#    time  = db.DateTimeProperty(auto_now_add=True)
#    judges = db.IntegerProperty(default=0) # number of judges submitted (judge = dialog, where user makes and submits some judgment)
#    new_entities = db.IntegerProperty(default=0) # number of entities created
#
#def SetParameters(c): # Set operational parameters according to data in c [Counters ]
#    d = 60*60*24//cCountersPeriod # data multiplier for 24h
#    global cPublicImps
#    cPublicImps = d*c.judges
#
#def GetCounters():
#    c = Counters.all().order('-time').get()
#    if not c or c.time + cCountersPeriod > datetime.datetime.now():
#        c = Counters()
#    return c
#